package services

import (
	"com.opennews.openplatform/web_api/domain"
	"com.opennews.openplatform/web_api/security"
	"com.opennews.openplatform/web_api/shared"
	"strings"
	"time"
)

// region: Public members

type AuthenticationService interface {
	AuthenticateByCredential(username string, password string) (myUserDetails security.MyUserDetails, errorCode string)
	AuthenticateByAuthCode(username string, authCode string) (myUserDetails security.MyUserDetails, errorCode string)
	ValidateToken(tokenString string) (claims *security.MyClaims, valid bool)
	RefreshToken(refreshToken string) (myUserDetails security.MyUserDetails, errorCode string)
}

// Returns the instance of private member, userService, which implements the userService interface.
func NewAuthenticationService() AuthenticationService {
	return authenticationService{
		userService: NewUserService(),
	}
}

// Authenticates user based on username and password.
func (this authenticationService) AuthenticateByCredential(username string, password string) (myUserDetails security.MyUserDetails, errorCode string) {
	myUserDetails = security.MyUserDetails{}
	errorCode = ""

	// TODO: Uses userService to retrieve real user info, such like roles and etc.
	// Uses real logic to check user credential with userService in PRO usage.
	if username == "Hui" && password == "1" {
		myUserDetails = this.generateUserDetails(username)
	} else {
		// If the user input is not correct.
		// Actually we did not give info like 'user not found' or 'password is incorrect'.
		// This is for void brute force attack. If the attacker does NOT know the login id or password is correct that would make the attach more difficult.
		errorCode = shared.ErrorUserAccountBadCredential
	}

	return myUserDetails, errorCode
}

// Authenticates user based on username and auth code.
func (this authenticationService) AuthenticateByAuthCode(username string, authCode string) (myUserDetails security.MyUserDetails, errorCode string) {
	myUserDetails = security.MyUserDetails{}
	errorCode = ""

	// TODO: Uses userService to retrieve real user info, such like roles and etc.
	// Uses real logic to check user auth code with userService in PRO usage.
	if username == "Hui" && authCode == "1234" {
		myUserDetails = this.generateUserDetails(username)
	} else {
		// If the user input is not correct.
		// Actually we did not give info like 'user not found' or 'authCode is incorrect'.
		// This is for void brute force attack. If the attacker does NOT know the login id or authCode is correct that would make the attach more difficult.
		errorCode = shared.ErrorUserAccountBadCredential
	}

	return myUserDetails, errorCode
}

// Checks on access/refresh token to see if it is a valid one and does not expire yet.
// Refresh token does not check on expiration.
func (this authenticationService) ValidateToken(tokenString string) (claims *security.MyClaims, valid bool) {
	// Gets current configuration.
	appConfig := shared.ConfigService.GetAppConfig()

	// Sets default result.
	valid = false

	// Parses token string to claims and token structure.
	claims, token, err := security.ParseToken(tokenString, appConfig.Jwt.SigningKey)

	// Checks if token is valid and does not expires yet.
	// VerifyExpiresAt takes 2nd parameter as false means refresh token does not to check on expiration. Because refresh token sets expiresAt = 0.
	if err == nil && token.Valid && claims.VerifyExpiresAt(shared.GetCurrentTime().Unix(), false) {
		// TODO: Checks if token is currently active in datasource if needed.
		valid = true
	}

	return claims, valid
}

func (this authenticationService) RefreshToken(refreshToken string) (myUserDetails security.MyUserDetails, errorCode string) {
	myUserDetails = security.MyUserDetails{}
	errorCode = ""

	myClaims, valid := this.ValidateToken(refreshToken)

	if valid {
		// TODO: Uses real logic to check if refreshToken is currently active in datasource.
		// TODO: Retrieve real user info via userService.

		// Gets the configured signing key and expiration.
		appConfig := shared.ConfigService.GetAppConfig()

		// For access token which will expires in particular duration.
		claims := security.MyClaims{
			BaseClaims: myClaims.BaseClaims,
			Roles:      []string{"Admin"},
		}

		// Generates access token.
		accessTokenString, _ := security.GenerateToken(&claims, appConfig.Jwt.SigningKey, appConfig.Jwt.Expiration)

		// For client usage.
		fullRoles := []security.FullRole{
			{
				ID:        "1",
				Authority: "Admin",
				Title:     "管理员",
			},
		}

		// Prepares MyUserDetails data.
		myUserDetails.ID = "user-id"
		myUserDetails.Username = "Hui"
		myUserDetails.FullName = "Hui Wang"
		myUserDetails.AvatarUrl = "http://"
		myUserDetails.Bearer = "bearer"
		myUserDetails.FullRoles = fullRoles
		myUserDetails.AccessToken = accessTokenString
		myUserDetails.RefreshToken = refreshToken
		myUserDetails.GeneratedAt = time.Unix(claims.IssuedAt, 0).Format(shared.LongDateTimeFormat)
		myUserDetails.ExpiredAt = time.Unix(claims.ExpiresAt, 0).Format(shared.LongDateTimeFormat)

	} else {
		errorCode = shared.ErrorUserAccountNotAuthorized
	}

	return myUserDetails, errorCode
}

// endregion

// region: Private members

// Generates MyUserDetails based on username.
func (this authenticationService) generateUserDetails(username string) security.MyUserDetails {
	myUserDetails := security.MyUserDetails{}

	if strings.TrimSpace(username) != "" {
		// TODO: Uses userService to retrieve real user info, such like roles and etc.
		//user := userService.GetByUsername(username)

		user := domain.User{
			Id:        "user-id",
			Username:  "Hui",
			FullName:  "Hui Wang",
			AvatarUrl: "http://",
		}

		// Gets the configured signing key and expiration.
		appConfig := shared.ConfigService.GetAppConfig()

		// For refresh token which does not need Roles and never expires.
		baseClaims := security.BaseClaims{
			UserIdentity: security.UserIdentity{
				ID:       user.Id,
				Username: user.Username,
			},
		}

		// Generates refresh token.
		refreshTokenString, _ := security.GenerateToken(&baseClaims, appConfig.Jwt.SigningKey, 0)

		// For access token which will expires in particular duration.
		claims := security.MyClaims{
			BaseClaims: baseClaims,
			Roles:      []string{"Admin"},
		}

		// Generates access token.
		accessTokenString, _ := security.GenerateToken(&claims, appConfig.Jwt.SigningKey, appConfig.Jwt.Expiration)

		// For client usage.
		fullRoles := []security.FullRole{
			{
				ID:        "1",
				Authority: "Admin",
				Title:     "管理员",
			},
		}

		// Prepares MyUserDetails data.
		myUserDetails.ID = user.Id
		myUserDetails.Username = user.Username
		myUserDetails.FullName = user.FullName
		myUserDetails.AvatarUrl = user.AvatarUrl
		myUserDetails.Bearer = "bearer"
		myUserDetails.FullRoles = fullRoles
		myUserDetails.AccessToken = accessTokenString
		myUserDetails.RefreshToken = refreshTokenString
		myUserDetails.GeneratedAt = time.Unix(claims.IssuedAt, 0).Format(shared.LongDateTimeFormat)
		myUserDetails.ExpiredAt = time.Unix(claims.ExpiresAt, 0).Format(shared.LongDateTimeFormat)
	}

	return myUserDetails
}

// Container of AuthenticationService functions.
type authenticationService struct {
	userService UserService
}

// endregion
